#!/usr/bin/env bash
# Test stub for `gh`. Logs the call; returns env-configured canned output.
# Replaces real gh on PATH during hermetic verb tests so we assert control flow
# (which mutating calls happen, in what order) without re-implementing gh. The
# real gh idioms are proven by the live smoke (cycle DoD), not here.
#
# Issue-anchor model (Cycle 12): the scripts call `gh issue *` for the handoff
# artifact, and `gh pr *` only for linked-PR live-state derivation in rehydrate.
#
# State surface (per-test, via env):
#   STUB_ME                  Current user's login (default: alien8d). The script
#                            reads this via `gh api user --jq .login` to compare
#                            against an issue's assignees set.
#   STUB_ISSUE_NUMBER        Default issue number when one needs to be invented.
#   STUB_ISSUE_TITLE         Title for `gh issue view --jq .title`.
#   STUB_ISSUE_BODY          Default body for `gh issue view --jq .body`.
#   STUB_ISSUE_BODY_2        Override for the 2ND `--jq .body` call (race-safety
#                            seam: emulates a concurrent writer mutating the
#                            body between the initial fetch and the pre-PATCH
#                            re-fetch per spec §7).
#   STUB_ISSUE_BODY_3        Override for the 3RD call (rarely needed).
#   STUB_ISSUE_STATE         OPEN | CLOSED (default OPEN).
#   STUB_ISSUE_STATE_2       Override on the 2ND state-bearing view (drift seam).
#   STUB_ISSUE_ASSIGNEES     CSV of logins ('' = no assignee). e.g. 'alien8d'
#                            or 'other' or 'alien8d,other'.
#   STUB_ISSUE_ASSIGNEES_2   Override on the 2ND assignees-bearing view (drift).
#   STUB_ISSUE_ASSIGNEES_3   Override on the 3RD call (post-assign verify).
#   STUB_ISSUE_LABELS        CSV of label names (default 'handoff').
#   STUB_ISSUE_UPDATED_AT    Default updatedAt (default fixed timestamp).
#   STUB_ISSUE_UPDATED_AT_2  Override for the 2ND view (drift seam).
#   STUB_ISSUE_CLOSED_AT     For CLOSED issues: timestamp (default '').
#   STUB_ISSUE_VIEW_RC       gh issue view exit code (default 0).
#   STUB_ISSUE_EDIT_RC       gh issue edit exit code (default 0).
#   STUB_ISSUE_CLOSE_RC      gh issue close exit code (default 0).
#   STUB_ISSUE_CREATE_NUMBER Number returned by `gh issue create` (default 999).
#   STUB_ISSUE_CREATE_URL    URL returned by `gh issue create` (default sensible).
#   STUB_ISSUE_LIST          Raw JSON array string returned by `gh issue list`.
#                            If unset, the stub returns [].
#   STUB_ISSUE_LIST_2        Override for the 2ND list call (no-arg find-existing
#                            followed by stack list, etc.).
#   STUB_PR_REFS             CSV of "ref|title|state|merge|review" rows. Used by
#                            `gh pr view <ref> --json ...` for linked-PR rehydrate
#                            and for per-link title fetch. Stub matches on <ref>.
#   STUB_PR_LIST_FOR_BRANCH  JSON array for `gh pr list --head <branch>`.
#                            (Auto-link path on /handoff no-arg.)
#   STUB_AUTH_RC             gh auth status exit code (default 0).
set -euo pipefail

# Log every call (verbatim args) for control-flow assertions in tests.
printf 'gh %s\n' "$*" >> "${GH_CALLS:-/dev/null}"

# Parse out --jq, --json, --method (positional-walk so we tolerate any order).
prev=""; jqf=""; jsonf=""; method=""; bodyfile=""
for a in "$@"; do
  case "$prev" in
    --jq)        jqf="$a" ;;
    --json)      jsonf="$a" ;;
    --method)    method="$a" ;;
    --body-file) bodyfile="$a" ;;
  esac
  prev="$a"
done
# Reference unused-on-some-paths vars so shellcheck stays quiet.
: "$method" "$jsonf"

# Per-call counter shared across the test process. Keyed by counter name so
# distinct call types (issue_view, issue_view_body, etc.) increment
# independently. Counter files live next to $GH_CALLS so newenv's wipe of $TMP
# also wipes them.
_bump_counter() {
  local key="$1"
  local dir; dir="$(dirname "${GH_CALLS:-/tmp/.gh-calls}")"
  local cf="$dir/.stub_counter_${key}"
  local n
  n="$(cat "$cf" 2>/dev/null || echo 0)"
  n=$((n + 1))
  printf '%s' "$n" > "$cf"
  printf '%s' "$n"
}

# Return $1's value if set, else $2's value (default). Used for per-call
# overrides: `_pick STUB_ISSUE_BODY_2 STUB_ISSUE_BODY`.
_pick() {
  local k1="$1" k2="$2"
  if [ -n "${!k1+x}" ]; then printf '%s' "${!k1}"
  else printf '%s' "${!k2-}"
  fi
}

# Build a canonical issue JSON blob from STUB_ISSUE_* vars. Always carries every
# field the scripts might `--jq` extract; jq filters out what's not asked for.
# $1 is the suffix used for per-call overrides:
#   "_${n}"  for body-bearing calls (body counter; existing _2/_3 conventions)
#   "_V${n}" for body-less calls (issue_view counter; new namespace for tests
#            that need to differentiate validate-body-vs-drift-no-body)
_issue_blob() {
  local suffix="$1"
  local number title body state assignees_csv labels_csv updated_at closed_at
  number="${STUB_ISSUE_NUMBER:-42}"
  title="${STUB_ISSUE_TITLE:-Handoff: example}"
  body="$(_pick "STUB_ISSUE_BODY${suffix}" STUB_ISSUE_BODY)"
  state="$(_pick "STUB_ISSUE_STATE${suffix}" STUB_ISSUE_STATE)"; state="${state:-OPEN}"
  assignees_csv="$(_pick "STUB_ISSUE_ASSIGNEES${suffix}" STUB_ISSUE_ASSIGNEES)"
  labels_csv="${STUB_ISSUE_LABELS:-handoff}"
  updated_at="$(_pick "STUB_ISSUE_UPDATED_AT${suffix}" STUB_ISSUE_UPDATED_AT)"
  updated_at="${updated_at:-2026-05-30T00:00:00Z}"
  closed_at="${STUB_ISSUE_CLOSED_AT-}"
  jq -n \
    --argjson number "$number" \
    --arg title "$title" \
    --arg body "$body" \
    --arg state "$state" \
    --arg updated_at "$updated_at" \
    --arg closed_at "$closed_at" \
    --arg ass "$assignees_csv" \
    --arg lab "$labels_csv" \
    '{
       number: $number,
       title:  $title,
       body:   $body,
       state:  $state,
       updatedAt: $updated_at,
       closedAt: (if $closed_at == "" then null else $closed_at end),
       assignees: ($ass | split(",") | map(select(length>0) | {login: .})),
       labels:    ($lab | split(",") | map(select(length>0) | {name: .}))
     }'
}

# Build a canonical PR JSON blob for `gh pr view <ref>`. Driven by STUB_PR_REFS,
# a CSV-of-pipe-rows: 'ref|title|state|mergeStateStatus|reviewDecision'.
# Matches by ref; falls through to a generic blob if no row matches.
_pr_blob() {
  local ref="$1"
  local row title state merge review
  if [ -n "${STUB_PR_REFS:-}" ]; then
    row="$(printf '%s' "$STUB_PR_REFS" | tr ',' '\n' | grep -E "^${ref}\|" | head -1)"
    if [ -n "$row" ]; then
      title="$( printf '%s' "$row" | awk -F'|' '{print $2}')"
      state="$( printf '%s' "$row" | awk -F'|' '{print $3}')"
      merge="$( printf '%s' "$row" | awk -F'|' '{print $4}')"
      review="$(printf '%s' "$row" | awk -F'|' '{print $5}')"
    fi
  fi
  title="${title:-stub PR ${ref}}"
  state="${state:-OPEN}"
  merge="${merge:-CLEAN}"
  review="${review:-APPROVED}"
  jq -n \
    --argjson number "$ref" \
    --arg title "$title" \
    --arg state "$state" \
    --arg merge "$merge" \
    --arg review "$review" \
    '{number: $number, title: $title, state: $state,
      mergeStateStatus: $merge, reviewDecision: $review,
      headRefName: "stub-head", statusCheckRollup: []}'
}

# Apply --jq filter to a blob if --jq present; else print blob.
_emit_blob() {
  local blob="$1"
  if [ -n "$jqf" ]; then
    printf '%s' "$blob" | jq -r "$jqf"
  else
    printf '%s' "$blob"
  fi
}

case "$1" in

  auth)
    exit "${STUB_AUTH_RC:-0}"
    ;;

  api)
    # `gh api user --jq .login` — used by scripts to learn @me's login.
    if [ "${2:-}" = "user" ]; then
      printf '%s' "${STUB_ME:-alien8d}"
      exit 0
    fi
    exit 0
    ;;

  label)
    # `gh label create handoff ...` — idempotent in tests.
    exit 0
    ;;

  issue)
    case "${2:-}" in

      view)
        if [ "${STUB_ISSUE_VIEW_RC:-0}" -ne 0 ]; then exit "${STUB_ISSUE_VIEW_RC}"; fi
        # Distinct counter for body-bearing fetches so race-safety overrides
        # (STUB_ISSUE_BODY_2) only affect those, not unrelated state queries.
        # ANY call that includes body in --json (alone OR mixed with other
        # fields like state,labels,assignees,body) increments the body counter
        # and uses the "_N" override naming. Body-less calls (drift / post-
        # assign verify) use the "_VN" namespace so accept-chain tests can
        # override validate-time vs drift-time independently.
        is_body_call=0
        [ "$jqf" = ".body" ] && is_body_call=1
        if [ -n "$jsonf" ] && printf '%s' "$jsonf" | tr ',' '\n' | grep -qx body; then
          is_body_call=1
        fi
        if [ $is_body_call -eq 1 ]; then
          slot_n="$(_bump_counter issue_view_body)"
          slot_suffix="_${slot_n}"
        else
          slot_n="$(_bump_counter issue_view)"
          slot_suffix="_V${slot_n}"
        fi
        _emit_blob "$(_issue_blob "$slot_suffix")"
        ;;

      list)
        if [ "${STUB_ISSUE_LIST_RC:-0}" -ne 0 ]; then exit "${STUB_ISSUE_LIST_RC}"; fi
        slot="$(_bump_counter issue_list)"
        raw="$(_pick "STUB_ISSUE_LIST_${slot}" STUB_ISSUE_LIST)"
        [ -z "$raw" ] && raw="[]"
        _emit_blob "$raw"
        ;;

      create)
        # Snapshot the body-file content so tests can inspect it after the
        # script's mktemp -d workdir is wiped by its EXIT trap.
        if [ -n "$bodyfile" ] && [ "$bodyfile" != "-" ] && [ -r "$bodyfile" ]; then
          cp "$bodyfile" "$(dirname "$GH_CALLS")/last_create_body.md"
        fi
        num="${STUB_ISSUE_CREATE_NUMBER:-999}"
        url="${STUB_ISSUE_CREATE_URL:-https://github.com/o/r/issues/${num}}"
        if [ -n "$jqf" ]; then
          jq -n --argjson n "$num" --arg u "$url" '{number:$n, url:$u}' | jq -r "$jqf"
        else
          printf '%s\n' "$url"
        fi
        ;;

      edit)
        if [ "${STUB_ISSUE_EDIT_RC:-0}" -ne 0 ]; then exit "${STUB_ISSUE_EDIT_RC}"; fi
        if [ -n "$bodyfile" ] && [ "$bodyfile" != "-" ]; then
          if [ ! -r "$bodyfile" ]; then
            printf 'stub gh issue edit: body-file unreadable: %s\n' "$bodyfile" >&2
            exit 2
          fi
          # Snapshot for test inspection.
          cp "$bodyfile" "$(dirname "$GH_CALLS")/last_edit_body.md"
        fi
        ;;

      close)
        exit "${STUB_ISSUE_CLOSE_RC:-0}"
        ;;

      *)
        : ;;
    esac
    ;;

  pr)
    case "${2:-}" in

      view)
        if [ "${STUB_PR_VIEW_RC:-0}" -ne 0 ]; then exit "${STUB_PR_VIEW_RC}"; fi
        ref="${3:-0}"
        _emit_blob "$(_pr_blob "$ref")"
        ;;

      list)
        raw="${STUB_PR_LIST_FOR_BRANCH:-[]}"
        _emit_blob "$raw"
        ;;

      *)
        : ;;
    esac
    ;;

  *)
    : ;;
esac
exit 0
